Skip to content

feat: 个人主页 /u/[username] (MVP)#284

Merged
longsizhuo merged 15 commits intomainfrom
feat/user-profile-page
Apr 16, 2026
Merged

feat: 个人主页 /u/[username] (MVP)#284
longsizhuo merged 15 commits intomainfrom
feat/user-profile-page

Conversation

@longsizhuo
Copy link
Copy Markdown
Member

Summary

MVP 个人主页,响应 Agent Teams 设计讨论。

  • 路由 /u/[username] SSR + ISR 300s
  • 布局 Bento 12-col grid(gap-8 松散),左 col-span-5 Identity 块 + 右 col-span-7 小卡网格(sm:grid-cols-2)
  • 三种小卡 PROJ / PAPER / DOC,SEC 编号 editorial 风格
  • 交互 hover 展开详情(max-height 原地展开),mobile tap toggle(统一一套 CSS + useState)
  • 数据源 后端 GET /api/user-center/profile/{username} + build-time site-leaderboard.json

配套后端改动(已直推 main,involutionhell-backend)

  • commit 8efae54: 新增 GET /api/user-center/profile/{username} 公开读接口 + SaToken 白名单
  • commit 8224e74: 修复 OAuth callback 缺参时 500 白屏(required=false + null check)
  • commit fdc376a: 新增 POST /analytics/events 埋点写入(前端 PR feat: i18n 双语系统 + 150 篇文档翻译 + Hero UX 迭代 #281 已配套切换)

同步固化开发文档

  • docs/architecture/frontend-backend-separation.md 首次写下前后端分离约定:
    • Next.js 只做渲染 / i18n / BFF
    • Java 独占业务逻辑 + DB 读写
    • 浏览器 → Vercel rewrite → Java(零 CORS,rewrite 不吃 Fluid CPU)
    • env 不做硬编码 fallback

MVP 砍刀记录(等 V2)

  • ❌ 编辑页(后端已有 PATCH /preferences,UI 下一迭代)
  • ❌ sticky 左大块(滚动行为先保守)
  • ❌ 绝对定位浮层(用 max-height 替代更稳)
  • ❌ Zotero 实时关联(pinned_papers 直接存 title/author/year)

已知问题(不阻塞此 PR)

  • 后端部署 workflow 自 2026-04-14 起持续失败(与本 PR 无关),GraalVM native image 构建产物容器反复重启。前三个提交(fcbf6a269727c5fdc376a)都走了同样的失败路径自动回滚到上个版本。
  • 这意味着 /u/[username] 页面在生产环境暂时会显示 404(后端 /api/user-center/profile/{username} 还没上线)。等后端部署修好自然打通。
  • Vercel Preview 因为 BACKEND_URL 指向 production Java(尚未更新),同样会 404。本地用 .env.local 指向本地 Java 可验证完整链路

Test plan

  • pnpm build 通过
  • pnpm lint / pnpm test 通过
  • 本地启后端 8080 + 前端 3010,访问 /u/longsizhuo 看 Bento 渲染
  • mobile 视图(≤640px)单列 + tap 展开正常
  • desktop hover 展开过渡丝滑
  • 用户不存在时 404 页面正常

MVP 范围(UX + PM 对齐后砍刀):
- 路由:/u/[username] SSR(ISR 300s)
- 数据源:后端 GET /api/user-center/profile/{username}
- 布局:12-col bento,左 col-span-5 Identity,右 col-span-7 小卡网格
- 间距:gap-8 统一,用户明确要求松散
- 三种卡片:PROJ / PAPER / DOC,前两者读 preferences,DOC 读 leaderboard
- hover 展开用 max-height 原地展开;mobile 改 tap toggle

砍掉(V2):
- sticky 左大块(滚动竞争)
- 绝对定位浮层(移动端 mess)
- 编辑页(复用后端 PATCH /preferences)
- Zotero 实时关联(pinned_papers 直接存 title/author/year)

配套后端改动见 involutionhell-backend main commit 8efae54
(新增 GET /api/user-center/profile/{username} + SaToken 白名单)

同步新增:
- docs/architecture/frontend-backend-separation.md 固化前后端分离约定
Copilot AI review requested due to automatic review settings April 15, 2026 19:43
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
involutionhell-github-io Ready Ready Preview, Comment Apr 16, 2026 6:21pm
website-preview Ready Ready Preview, Comment Apr 16, 2026 6:21pm

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an MVP user profile page at /u/[username] and documents the intended frontend/backend separation contract for the project, aligning with the recent Agent Teams design discussion.

Changes:

  • Introduce SSR profile route /u/[username] that fetches user profile/preferences from the Java backend and combines it with build-time leaderboard data.
  • Add reusable client ProfileCard component with hover/tap expand behavior for projects/papers/docs.
  • Add a new architecture doc formalizing Next.js vs Java responsibilities, rewrites, and env var expectations.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 8 comments.

File Description
docs/architecture/frontend-backend-separation.md New architecture/contract doc for frontend-backend separation and env/url conventions
app/u/[username]/page.tsx New SSR profile page with ISR-style fetch revalidation and leaderboard-based doc contributions
app/u/[username]/ProfileCard.tsx New interactive profile card component (expand on hover/tap) for profile content blocks

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread app/u/[username]/page.tsx Outdated
Comment on lines +58 to +76
* 失败或 404 返回 null,让页面走 notFound()。
*/
async function fetchProfile(
username: string,
): Promise<ProfileResponse["data"] | null> {
const backendUrl = process.env.BACKEND_URL;
if (!backendUrl) return null;
try {
const res = await fetch(
`${backendUrl}/api/user-center/profile/${encodeURIComponent(username)}`,
// 用户主页数据变化慢(preferences 手动编辑),缓 5 分钟已足够
{ next: { revalidate: 300 } },
);
if (!res.ok) return null;
const json = (await res.json()) as ProfileResponse;
if (!json.success || !json.data) return null;
return json.data;
} catch {
return null;
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetchProfile 里把所有非 2xx(包括 500/502/超时网关等)都映射成 nullnotFound(),会把“后端故障/配置错误”伪装成“用户不存在”。建议至少区分 404 才 notFound,其他状态应抛错进入 error boundary 或返回一个可观测的失败状态(并在非生产环境打印告警)。

Suggested change
* 失败或 404 返回 null,让页面走 notFound()
*/
async function fetchProfile(
username: string,
): Promise<ProfileResponse["data"] | null> {
const backendUrl = process.env.BACKEND_URL;
if (!backendUrl) return null;
try {
const res = await fetch(
`${backendUrl}/api/user-center/profile/${encodeURIComponent(username)}`,
// 用户主页数据变化慢(preferences 手动编辑),缓 5 分钟已足够
{ next: { revalidate: 300 } },
);
if (!res.ok) return null;
const json = (await res.json()) as ProfileResponse;
if (!json.success || !json.data) return null;
return json.data;
} catch {
return null;
* 404 返回 null,让页面走 notFound();其他失败抛错进入 error boundary。
*/
function warnFetchProfile(message: string, details?: Record<string, unknown>) {
if (process.env.NODE_ENV !== "production") {
console.warn(`[fetchProfile] ${message}`, details ?? {});
}
}
async function fetchProfile(
username: string,
): Promise<ProfileResponse["data"] | null> {
const backendUrl = process.env.BACKEND_URL;
if (!backendUrl) {
warnFetchProfile("Missing BACKEND_URL", { username });
throw new Error("BACKEND_URL is not configured");
}
try {
const res = await fetch(
`${backendUrl}/api/user-center/profile/${encodeURIComponent(username)}`,
// 用户主页数据变化慢(preferences 手动编辑),缓 5 分钟已足够
{ next: { revalidate: 300 } },
);
if (res.status === 404) return null;
if (!res.ok) {
warnFetchProfile("Backend returned a non-404 error response", {
username,
status: res.status,
statusText: res.statusText,
});
throw new Error(
`Failed to fetch profile for "${username}": ${res.status} ${res.statusText}`,
);
}
const json = (await res.json()) as ProfileResponse;
if (!json.success || !json.data) {
warnFetchProfile("Backend returned an invalid profile payload", {
username,
success: json.success,
hasData: !!json.data,
message: json.message,
});
throw new Error(`Invalid profile response for "${username}"`);
}
return json.data;
} catch (error) {
warnFetchProfile("Profile fetch failed", {
username,
error: error instanceof Error ? error.message : String(error),
});
throw error;

Copilot uses AI. Check for mistakes.
Comment thread app/u/[username]/page.tsx Outdated
Comment on lines +202 to +207
<a
key={link.url}
href={link.url}
target="_blank"
rel="noopener noreferrer"
className="font-mono text-[10px] uppercase tracking-widest px-2 py-1 border border-[var(--foreground)] hover:bg-[var(--foreground)] hover:text-[var(--background)] transition-colors"
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里直接把 link.url 渲染进 <a href>(且 target=_blank),如果后端返回了 javascript:/data: 之类的危险 scheme,会形成可点击的 XSS / 钓鱼向量。建议在渲染前做 URL 白名单校验(例如仅允许 http/https,必要时允许 mailto),不合法则不渲染或降级为纯文本。

Copilot uses AI. Check for mistakes.
Comment thread app/u/[username]/ProfileCard.tsx Outdated
Comment on lines +45 to +50
<article
// onClick 提供 mobile tap toggle;desktop 的 hover 通过 group-hover 样式实现
onClick={() => setExpanded((v) => !v)}
className={[
"group relative border border-[var(--foreground)] bg-[var(--background)]",
"p-6 flex flex-col gap-3 min-h-[180px] cursor-pointer",
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个卡片在 <article> 上绑定了 onClick,但目前会在 desktop 也触发“点击展开”并与注释里“mobile tap toggle”不一致,容易导致桌面端出现“离开 hover 仍保持展开”的意外状态。建议只在不支持 hover 的设备上启用 click toggle(例如用 CSS @media (hover: none) + 仅在该条件下挂事件,或在 handler 里通过 matchMedia 判断)。

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +54
<article
// onClick 提供 mobile tap toggle;desktop 的 hover 通过 group-hover 样式实现
onClick={() => setExpanded((v) => !v)}
className={[
"group relative border border-[var(--foreground)] bg-[var(--background)]",
"p-6 flex flex-col gap-3 min-h-[180px] cursor-pointer",
"transition-shadow duration-200 ease-out",
"hover:shadow-[6px_6px_0_var(--foreground)]",
spanFull ? "sm:col-span-2" : "",
].join(" ")}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

当前整个卡片是可点击区域(onClick + cursor-pointer),但元素是 <article> 且没有 role, tabIndex, 键盘事件处理或 aria-expanded,键盘/读屏用户无法发现或触发展开交互。建议把可交互容器改成语义化的 <button>/<details>,或至少补齐 role="button"tabIndex={0}onKeyDown(Enter/Space) 与 aria-expanded

Copilot uses AI. Check for mistakes.
Comment thread app/u/[username]/ProfileCard.tsx Outdated
Comment on lines +99 to +105
{href && (
<div className="mt-auto pt-3">
<Link
href={href}
target={href.startsWith("http") ? "_blank" : undefined}
rel={href.startsWith("http") ? "noopener noreferrer" : undefined}
onClick={(e) => e.stopPropagation()}
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

href 来自用户可编辑的 preferences(projects/papers),这里直接喂给 <Link href> 并可能 target=_blank。若 href 包含 javascript:/data: 等 scheme,会变成可点击的注入向量。建议在组件内(或上游统一)对 href 做协议白名单校验,仅允许 http/https(必要时 mailto),不合法则不渲染链接按钮。

Copilot uses AI. Check for mistakes.
Comment on lines +96 to +103
**不做硬编码 fallback**(`?? "http://localhost:8081"` 这种禁止)。理由:端口不一致(8080/8081/其他),fallback 到错的端口会让配置漏配变成静默失败。

| 变量 | 前端读取场景 | 设置位置 |
| ------------------------- | --------------------------- | -------------------------------------- |
| `BACKEND_URL` | Server 端(SSR、API Route) | `.env.local` (dev) / Vercel env (prod) |
| `NEXT_PUBLIC_BACKEND_URL` | 浏览器端(HotDocsTab 这种) | 同上 |

`.env.sample` 里保留 `BACKEND_URL=http://localhost:8080` 作为模板,开发者 `cp .env.sample .env.local` 后按自己后端端口改。
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里写了“不做硬编码 fallback?? "http://localhost:8081" 这种禁止)”,但仓库当前的 next.config.mjs rewrites 以及至少 app/api/upload/route.ts 仍在对 BACKEND_URL?? "http://localhost:8080" fallback。文档约定与现状不一致会误导后续开发;建议要么同步改代码移除 fallback 并在缺失时显式报错/提示,要么把文档改成“生产环境禁止 fallback,开发环境可默认 8080”等与现状一致的表述。

Suggested change
**不做硬编码 fallback**`?? "http://localhost:8081"` 这种禁止)。理由:端口不一致(8080/8081/其他),fallback 到错的端口会让配置漏配变成静默失败。
| 变量 | 前端读取场景 | 设置位置 |
| ------------------------- | --------------------------- | -------------------------------------- |
| `BACKEND_URL` | Server 端(SSR、API Route) | `.env.local` (dev) / Vercel env (prod) |
| `NEXT_PUBLIC_BACKEND_URL` | 浏览器端(HotDocsTab 这种) | 同上 |
`.env.sample` 里保留 `BACKEND_URL=http://localhost:8080` 作为模板,开发者 `cp .env.sample .env.local` 后按自己后端端口改。
**生产环境不做硬编码 fallback**。当前仓库里仍有少量开发态/历史路径使用 `BACKEND_URL ?? "http://localhost:8080"` 的写法,目的是本地联调时减少启动门槛;但这只是过渡状态,**不能作为长期约定依赖**。原因是端口不一致(8080/8081/其他)时,fallback 很容易把“漏配”变成“静默请求到错误地址”,排查成本更高。
因此,约定如下:
- **prod / preview**:必须显式配置 `BACKEND_URL`,缺失应视为配置错误
- **dev**:允许现有代码中的临时 localhost fallback,但新代码默认不要继续扩散这种模式;优先通过 `.env.local` 明确配置
- 如果后续把现有 fallback 全部移除,以代码行为为准,并同步更新本文档
| 变量 | 前端读取场景 | 设置位置 |
| ------------------------- | --------------------------- | -------------------------------------- |
| `BACKEND_URL` | Server 端(SSR、API Route) | `.env.local` (dev) / Vercel env (prod) |
| `NEXT_PUBLIC_BACKEND_URL` | 浏览器端(HotDocsTab 这种) | 同上 |
`.env.sample` 里保留 `BACKEND_URL=http://localhost:8080` 作为模板,开发者 `cp .env.sample .env.local` 后按自己后端端口改;不要因为“本地某些路径暂时能 fallback”就省略这一步。

Copilot uses AI. Check for mistakes.
Comment on lines +152 to +156
"title": "项目名",
"description": "一段描述",
"url": "https://...",
"tags": ["TypeScript", "LLM"],
},
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个 jsonc 示例里 projects[0].url 这一行缺少结尾的引号,导致整段示例不是合法的 JSONC(编辑器高亮/复制粘贴会出错)。建议补齐引号,并顺便检查示例中对象/数组的逗号是否符合期望的 JSONC 风格。

Copilot uses AI. Check for mistakes.
Comment thread app/u/[username]/page.tsx Outdated
}

/**
* SSR 获取用户主页数据。匿名请求,走 Next rewrite 到 Java 后端。
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

注释写“走 Next rewrite 到 Java 后端”,但这里实际是服务端直接 fetch(${BACKEND_URL}/...),不会经过 next.config.mjs rewrites。建议把注释改成“服务端直连后端(使用 BACKEND_URL)”或改成请求相对路径以真正走 rewrites,避免后续排查时产生误解。

Suggested change
* SSR 获取用户主页数据。匿名请求, Next rewrite Java 后端。
* SSR 获取用户主页数据。匿名请求,服务端使用 BACKEND_URL 直连 Java 后端。

Copilot uses AI. Check for mistakes.
- UserMenu 登录态增加"我的主页"链接(基于 githubId)
- AuthNav 透传 githubId 到 UserMenu
- ContributorRow 弹窗加"VIEW DOSSIER"跳站内个人主页,GitHub 外链保留
- page.tsx 改按 user.githubId 从 leaderboard JSON 匹配贡献数据
  (之前按 name 字符串匹配踩坑:leaderboard.name="longsizhuo" vs user.username="github_114939201")
- 新增 EditLinkIfOwner 客户端小组件,只在访问者 == 主页主人时渲染编辑入口
…apers

- 新 page.tsx 壳 + EditProfileForm 客户端表单
- useAuth 读当前用户,URL username 与 githubId / username 都不匹配时显示"只能编辑自己"
- GET /api/user-center/preferences 拉现有值 → 表单 state
- 提交走 PATCH /api/user-center/preferences(后端合并 JSONB 顶层 key 保留其他)
- RepeatableList 通用泛型组件支持 links/projects/papers 增删
- editorial 风格:border 无圆角 / SEC 编号 / 硬阴影 submit 按钮
删 Next API Route,next.config 加 rewrite 把 /api/docs/history 透传到 Java,
Vercel 不再跑 Node function 去调 GitHub API。后端带 Caffeine 10min 缓存 +
GITHUB_TOKEN。配套后端 commit 见 involutionhell-backend#main。
- 从 noreply 邮箱(1234567+alice@users.noreply.github.com) 离线提取 id→login 映射,
  14/21 命中直接拿到 login,剩下 7 才走 GitHub API。规避限流风险。
- DB 聚合时同时按日分桶贡献次数写进 dailyCounts: { "YYYY-MM-DD": count },
  供前端活跃度热力图渲染,零运行时 DB 查询。

generated/site-leaderboard.json 一并重新生成带 dailyCounts。
- 新增 ActivityHeatmap server component,无 JS
- 52 列 × 7 行小格子,色阶分 5 档(0 / 1-2 / 3-5 / 6-10 / 10+),硬红(#CC0000)系列
- 数据走 leaderboard.json 的 dailyCounts(零运行时 DB 查询,和 docs git-based 一致)
- 月份刻度 + 周几标签(周一/四/六)
- page.tsx Bento grid 下方独立一行展示,仅当有贡献数据时渲染
- FollowButton 客户端组件:拉 /api/user-center/follows/stats 填初始值,
  点击走 POST/DELETE /api/user-center/follows/{id},乐观更新失败回滚
- 自己访问自己主页只显示统计,不显示按钮
- 匿名用户可见数字,按钮显示"登录后关注"置灰
- prisma/schema.prisma 加 user_follows model 同步(DB 已在 Neon 手动建表)

配套后端端点见 involutionhell-backend#main 30daf9d
- GithubRepos server component,fetch /api/user-center/github/repos/{identifier}
- 2 列 grid,每卡显示 name / stars / description / language / 更新时间
- 数据为空时返回 null 不渲染 section

配套后端 commit involutionhell-backend b6b48b2(Caffeine 1h,不需要 OAuth scope 扩展)
- UserPaperItem 加 itemKey 字段(可选)
- page.tsx 提取所有 itemKey 批量调 /api/user-center/zotero/items,
  Zotero 拉到的元信息作为主数据源,手填字段作为离线 fallback
- EditProfileForm papers 区增加 itemKey 输入框(提示可选),保存条件放宽为 itemKey 或 title 至少有一个
- 完全向后兼容:历史 pinned_papers(只有 title/authors 的)照常渲染

配套后端 commit involutionhell-backend 1fb697f(Zotero API 代理 + Caffeine 1h)

V2 全部完成:#1 编辑页 / #2 docs/history 迁 Java / #3 活跃度热力图 /
#4 关注系统 / #5 GitHub repos 同步 / #6 保留 analyticsEvent / #7 Zotero itemKey
1. leaderboard as Row[] → leaderboard as unknown as Row[]
   JSON 字面量的 dailyCounts 各自是不同 literal 类型,和 Row 的 Record<string, number>
   索引签名不兼容,CI tsc --noEmit 报 TS2352。先经 unknown 绕开。

2. ProfileCard title prop 必填 string,但 UserPaperItem 引入 itemKey 后 title 变可选。
   兜底为 p.title || p.itemKey || "(untitled paper)"。
安全修复(P0):
- fetchProfile 只在后端真返 404 或 success=false 时 notFound();其他非 2xx 抛错进 error boundary
  (避免后端故障伪装成"用户不存在")
- links/projects/papers 的 URL 在渲染前过 sanitizeExternalUrl 白名单
  (仅 http/https/mailto + 相对路径,拦 javascript:/data: 等 XSS 向量)
- ProfileCard 内部再加 safeHref 二次防御

UX / a11y(P1):
- ProfileCard 的 click toggle 用 matchMedia('(hover: none)') 限定触屏设备
  (桌面端仍走 group-hover,避免"离开 hover 后仍保持展开"幽灵状态)
- 补 role="button" / tabIndex=0 / onKeyDown(Enter|Space) / aria-expanded,
  键盘 / 读屏用户可用;只在有 detail 时才挂这些属性

文档(P2):
- docs/architecture/frontend-backend-separation.md
  环境变量章节改成"生产禁 fallback,开发态过渡允许",与现状(next.config.mjs/upload 仍用 fallback)自洽
- fetchProfile 注释说"直连后端(BACKEND_URL)",不再错写"走 rewrite"
- 右侧 Bento 小卡区只保留 projects / papers,DOC 卡片抽出去
- 活跃度热力图从页底提到 Bento 之后立即显示(视觉重点前置)
- 新增 SEC. DOCS 独立 section 放页尾,紧凑列表(每行 ~48px 而非 180px 小卡)
- 默认只显示前 10 篇,超过的用 <details> 折叠"展开剩余 N 篇"

旧版顺序:Identity(bio/stats)+DOC 卡(8 条) → Heatmap → Repos
新版顺序:Identity(bio/stats)+projects/papers → Heatmap → Repos → Docs 列表(折叠)

文档特别多的用户(本作者 44 条)现在热力图和 repos 一屏可见,docs 列表放最后折叠。
- 去掉"首页":BrandMark 点击已回首页,避免冗余
- 去掉"特点":旧 Features 组件 2026-04 重构时删除,/#features 已失效
- 新增"文档"→ /docs 和"排行榜"→ /rank,把主要产品路由提到顶
- 保留"社区"→ /#community(DispatchNetwork bar)
- "联系我们"缩写为"联系"→ /#contact(Footer 还在)
用户反馈"编辑个人主页这地方不知道 edit 什么,不懂 papers 是什么意思"。
根因是字段名是工程师视角(bio/tagline/pinned_papers),普通用户不理解。

改动:
- 编辑页顶部加一段"About this page"总说明:填的东西去哪里显示、全都可选
- 每个 Section 从只有 SEC 编号,扩成「SEC 编号 + 中文大标题 + 一句话描述」
- PAPERS 块重命名为"最近在读 / 推荐的论文"(papers 太学术,降低理解门槛)
- placeholder 从抽象("项目名")换成具体示例("involutionhell.com")
- IDENTITY/LINKS/PROJECTS/PAPERS 4 块都加 heading + description 说明作用

主页空态文案:
- 旧:"该用户还没有填写 projects / papers"(用户反馈看不懂)
- 新:"Ta 还没填个人项目和最近在读的论文" + 小字补充"仍会显示 GitHub repos 和文档贡献"
基建:
- lib/i18n/messages.ts:扁平 key 字典,zh/en 同结构,{param} 占位填充
- lib/i18n/server.ts:server-only,getServerLocale + getServerT
- lib/i18n/client.tsx:LocaleProvider + useT hook
- app/layout.tsx:LocaleProvider 包在 ThemeProvider 外,locale 服务端读 cookie
  注入客户端 Context,避免 SSR/CSR 水合抖动

接入(全部 profile 相关 UI):
- page.tsx:dossier / stats / empty state / docs 列表全部走 t(key)
- ActivityHeatmap:改 async server component,月份 Jan..Dec 走 activity.month.{1..12}
- GithubRepos:heading / subtitle / count 走 t
- FollowButton / EditLinkIfOwner / ProfileCard:client hook useT
- edit/page.tsx + EditProfileForm:全套(intro / 4 个 section heading+description
  + placeholders + CTA + auth gate + RepeatableList 增删按钮文字)
- RepeatableList 新增 addLabel/removeLabel props,调用方翻译好后传入

验证:
- pnpm run typecheck 通过
- locale=zh → "活跃度/文档贡献/粉丝/积分/贡献过的文档/GitHub 仓库"
- locale=en → "Activity/Docs/Followers/Points/Docs Contributed/GitHub Repositories"
@longsizhuo longsizhuo merged commit ce84c11 into main Apr 16, 2026
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants